/***** BEGIN LICENSE BLOCK *****

    FlashGot - a Firefox extension for external download managers integration
    Copyright (C) 2004-2013 Giorgio Maone - g.maone@informaction.com

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
                             
***** END LICENSE BLOCK *****/

var Youtube = function(){

///////////////////////////////////////////////////////////////////////////////
// Public part.
var Youtube = {

  process: function (w, id, callback, isPrivate) {
    // First try to extract the stream data from the document:
    // either from ytplayer.config.args.url_encoded_fmt_stream_map
    // or from the player's flashvars.
    // If that fails, retry with /get_video_info.
    // XXX: The problem with using /get_video_info is that it allows
    // you to bypass the "This video is age-restricted" warning. But
    // I guess it's their problem that their script doesn't check what
    // it should check. Or did they leave that backdoor on purpose?
    // Or it's not a backdoor and the response contains some kind of
    // an "age-restricted" flag that the client must check?
    // You also can bypass it with /v/VIDEO_ID and /embed/VIDEO_ID.
    w["flashgot::Youtube::process::call_cnt"] = (w["flashgot::Youtube::process::call_cnt"] || 0) + 1;
    if (w["flashgot::Youtube::process::tid"]) {
      w.clearTimeout(w["flashgot::Youtube::process::tid"]);
      delete w["flashgot::Youtube::process::tid"];
    }

    function wait_for_player() {
      delete w["flashgot::Youtube::process::tid"];

      var now = Date.now();
      if (!arguments.callee["flashgot::start_time"]) {
        arguments.callee["flashgot::start_time"] = now;
      }
      if (now - arguments.callee["flashgot::start_time"] > 5000) {
        Youtube.do_xhr(w, id, callback, isPrivate);
        return;
      }
      var data;
      var o;
      // ytplayer.config.args
      if (!data && (o = w.wrappedJSObject) && (o = o.ytplayer) && (o = o.config) && (o = o.args) && o.url_encoded_fmt_stream_map) {
        data = o;
      }
      // <embed/> flashvars
      if (!data && (o = w.document)) {
        var ids = gFlashGotService.getPref("media.YouTube.flashvars_element_id", "movie_player").split(/\s+/).map(unescape);
        var attrs = gFlashGotService.getPref("media.YouTube.flashvars_element_attr", "flashvars").split(/\s+/).map(unescape);
        for (var i = 0, len = ids.length; !data && i !== len; ++i) {
          let id = ids[i];
          if (id.length === 0) { continue; }
          var elm = o.getElementById(id);
          if (elm === null) { continue; }
          for (var j = 0, jlen = attrs.length; j !== jlen; ++j) {
            var attr = attrs[j];
            if (attr.length !== 0 && (attr = elm.getAttribute(attr))) {
              data = Youtube.parse(attr);
              break;
            }
          }
        }
      }
      if (!data) {
        // The document is loaded, but still no data - no point in waiting further
        // (e.g. /embed/VIDEO_ID documents don't have the data we're looking for).
        if (w.document && w.document.readyState && (w.document.readyState === "interactive" || w.document.readyState === "complete")) {
          Youtube.do_xhr(w, id, callback, isPrivate);
        }
        else {
          w["flashgot::Youtube::process::tid"] = w.setTimeout(arguments.callee, 200);
        }
        return;
      }
      if (Youtube.process_video_info(data, callback)) {
        delete w["flashgot::Youtube::process::call_cnt"];
        return;
      }
      Youtube.do_xhr(w, id, callback, isPrivate);
    } // wait_for_player()
    w["flashgot::Youtube::process::tid"] = w.setTimeout(wait_for_player, 0);
  },

  do_xhr: function (w, id, callback, isPrivate) {
    var call_cnt = w["flashgot::Youtube::process::call_cnt"];

    var xhr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(CI.nsIXMLHttpRequest);
    xhr.open("GET", w.location.protocol + "//www.youtube.com/get_video_info?hl=en_US&el=detailpage&video_id="
      + id + "&sts=" + fg.getPref("media.YouTube.decode_signature_func.timestamp", -1));
    if (isPrivate) {
      fg.privatize(xhr.channel);
    }
    xhr.addEventListener("readystatechange", function (ev) {
      if (w["flashgot::Youtube::process::call_cnt"] !== call_cnt) {
        this.abort();
        return;
      };
      if (xhr.readyState !== 4) return;
      delete w["flashgot::Youtube::process::call_cnt"];
      Youtube.process_video_info(Youtube.parse(xhr.responseText || ""), callback);
    }, false);
    xhr.send(null);
  },

  parse: function (s) {
    var pairs = s.split("&");
    var res = {};
    var nv;
    for each(var p in pairs) {
      try {
        nv = p.split("=").map(function(v) { return decodeURIComponent(v.replace(/\+/g, ' ')) });
        if (!(nv[0] in res)) res[nv[0]] = nv[1];
      } catch (e) {}
    }
    return res;
  },

  process_video_info: function (data, callback) {
    if (!data || !data.url_encoded_fmt_stream_map) { return false; }
    var streams = data.url_encoded_fmt_stream_map.split(",").map(Youtube.parse);
    // 2013-10: 1080p is not in the map anymore (and some other formats as well).
    this.get_missing_streams(data, streams);
    if (!streams.length) { return false; }

    // Preprocess the streams:
    // 1) Mark 3D videos. MediaSniffer uses .quality ("small" (240p), "medium" (360p),
    // "hd720", etc.) and .type ("video/flv", "video/mp4", etc.) for formatting menu
    // item labels and tooltips.
    // 2) Decode the stream signatures.
    // 3) Build the sort keys. Don't sort right here,
    // because the responses can arrive in any order.
    var data_title = String(data.title);
    var signature_decoder;
    try {
      signature_decoder = this.create_signature_decoder();
    } catch (x) {
      fg.log("Error compiling YouTube.decode_signature_func: " + (x.message || x) + "\n" + x.stack);
      return false;
    }
    var remap_quality = fg.getPref("media.YouTube.remap_stream_quality", false);
    var remap_type = fg.getPref("media.YouTube.remap_stream_type", false);
    var quality_sort_order = remap_quality ? Youtube.REMAP_QUALITY_SORT_ORDER : Youtube.QUALITY_SORT_ORDER;
    var type_sort_order = remap_type ? Youtube.REMAP_TYPE_SORT_ORDER : Youtube.TYPE_SORT_ORDER;
    streams = streams.filter(function(s) {
      // We're only interested in HTTP streams, no RTMP or something.
      if (!/^https?:\/\//.test(s.url)) { return false; }

      // Save the video ID, for debugging purposes.
      s["flashgot::video_id"] = data.video_id;

      // Tidy the type: "video/3gpp;+codecs="mp4v.20.3,+mp4a.40.2" -> "video/3gpp".
      s.type = s.type.split(";")[0];
      if (remap_type) { s.type = Youtube.remap_stream_type(s); }

      if (remap_quality) { s.quality = Youtube.remap_stream_quality(s); }
      // Mark 3D videos.
      // http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
      if ((s.itag >= 82 && s.itag <= 85) || (s.itag >= 100 && s.itag <= 102)) {
        s.quality += " (3D)";
      }

      // Build the sort key: order by quality, type.
      s["flashgot::sort_key"] = (Youtube.get_sort_idx(quality_sort_order, s.quality) << 8)
        | Youtube.get_sort_idx(type_sort_order, s.type);
      // For DASH streams: audio goes after video.
      // So it's actually order by media type (audio or video), quality, container type.
      if (s["flashgot::yt_dash"] === "audio") {
        s["flashgot::sort_key"] |= 0x10000;
      }

      // Decode the signature.
      try {
        var sig = signature_decoder.decode( {stream: s, video_info: data, swap: Youtube.decode_signature_swap} );
        if (sig) { s.url += "&signature=" + encodeURIComponent(sig); }
      } catch (x) {
        fg.log("Error calling YouTube.decode_signature_func: " + (x.message || x) + "\n" + x.stack);
        return false;
      }

      // Tidy the URL.
      if (fg.getPref("media.YouTube.stream_url.tidy", false)) {
        var extra_keep_params = fg.getPref("media.YouTube.stream_url.extra_keep_params", "")
          .split(/\s+/)
          // URL-decode.
          .map(unescape)
          // Ignore empty items.
          .filter(function(o){return o.length !== 0;});
        Youtube.tidy_stream_url(s, extra_keep_params);
      }

      // Add "ratebypass" (speed limit) and "cmbypass" (no idea)
      // if they're not part of the signature.
      var idx = s.url.indexOf("?");
      if (idx !== -1 && idx + 1 !== s.url.length) {
        var qs = Youtube.parse(s.url.substr(idx + 1));
        var sparams = (qs.sparams || "").split(",");
        if (sparams.indexOf("ratebypass") === -1) { qs.ratebypass = "yes"; }
        if (sparams.indexOf("cmbypass") === -1) { qs.cmbypass = "yes"; }
        s.url = s.url.substr(0, idx + 1)
          + Object.keys(qs)
            .map(function(p){return encodeURIComponent(p) + "=" + encodeURIComponent(qs[p] || "")})
            .join("&");
      }

      return true;
    });
    signature_decoder.dispose();
    if (!streams.length) { return false; }
    callback( {title: data_title, streams: streams} );
    return true;
  }, // process_video_info()

  get_missing_streams: function(data, streams)
  {
    if (data.adaptive_fmts) {
      data.adaptive_fmts.split(",").map(Youtube.parse).forEach(function(o){
        var b = o.itag && o.type && o.url
          && (o.quality = Youtube._map_itag(Youtube.STREAM_QUALITY_MAP, o.itag, o.size || /*audio*/(Math.round(o.bitrate / 1024) + "k")));
        if (b) {
          o["flashgot::yt_dash"] = o.size ? "video" : "audio";
          streams.push(o);
        }
      });
    }

    // 2013-11: Doesn't work anymore - they added "itag" to the signature.
    if ( ! /(^|\.)youtube\.com\/api\/manifest\/dash\/(.+)/.test(data.dashmpd)) { return; }
    var a = RegExp.$2.split("/");
    if (a.length % 2) { a.push(""); }
    var dmp = {key: "yt1"};
    for (var i = 0, len = a.length; i !== len; i += 2) {
      dmp[decodeURIComponent(a[i])] = decodeURIComponent(a[i + 1]);
    }
    if ( ! dmp.signature || ! dmp.sparams) { return; }
    var dmp_keep_params = dmp.sparams.split(",").concat("sparams", "key");
    if (dmp_keep_params.indexOf("itag") !== -1) { return; }
    var stream_base = null;
    streams.some(function(s){
      if ( ! /^(https?:\/\/[^\/]+)\//.test(s.url)) { return false; }
      var host = RegExp.$1;
      if ( ! /[?&]id=([^&]+)/.test(s.url) || RegExp.$1 !== dmp.id) { return false; }
      stream_base = {
        fallback_host: s.fallback_host,
        sig: dmp.signature,
        url: host + "/videoplayback?"
          + dmp_keep_params.map(function(p) { return encodeURIComponent(p) + "=" + encodeURIComponent(dmp[p]) }).join("&")
      };
      return true;
    });
    if ( ! stream_base) { return; }
    // http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
    // Map<int itag, Object info> itag_map;
    var itag_map = {
      83 /* 240p mp4 */: {quality: "small", type: "video/mp4"},
      34 /* 360p flv */: {quality: "medium", type: "video/x-flv"},
      100 /* 360p 3D webm */: {quality: "medium", type: "video/webm"},
      101 /* 360p 3D webm */: {quality: "medium", type: "video/webm"},
      35 /* 480p flv */: {quality: "large", type: "video/x-flv"},
      44 /* 480p webm */: {quality: "large", type: "video/webm"},
      85 /* 520p mp4 */: {quality: "520p", type: "video/mp4"},
      45 /* 720p webm */: {quality: "hd720", type: "video/webm"},
      102 /* 720p 3D webm */: {quality: "hd720", type: "video/webm"},
      37 /* 1080p mp4 */: {quality: "hd1080", type: "video/mp4"},
      46 /* 1080p webm */: {quality: "hd1080", type: "video/webm"},
      38 /* 3072p mp4 aka "highres" aka "original" */: {quality: "highres", type: "video/mp4"}
    };
    // itags = select itag from itag_map where itag not in (select itag from streams);
    var itags = Object.keys(itag_map).filter(function(k) { return streams.every(function(s) { return k != s.itag }) });
    itags.forEach(function(k){
      var itag = itag_map[k];
      var stream = {
        itag: k,
        quality: itag.quality,
        type: itag.type,
        sig: stream_base.sig,
        fallback_host: stream_base.fallback_host,
        url: stream_base.url + "&itag=" + k
      };
      streams.push(stream);
    });
  },

  QUALITY_SORT_ORDER: ["highres" /*"original"*/, "hd1080", "hd1080 (3D)", "hd720", "hd720 (3D)", "large", "large (3D)", "medium", "medium (3D)", "small", "light", "tiny"],
  REMAP_QUALITY_SORT_ORDER: [
    "Original" /*highres*/, "1080p", "1080p (3D)", "720p", "720p (3D)", "520p", "480p", "480p (3D)", "360p", "360p (3D)", "270p", "240p", "144p",
    /*audio*/ "256k", "128k", "48k"
  ],
  TYPE_SORT_ORDER: ["video/mp4", "video/x-flv", "video/webm", "video/3gpp", "audio/mp4"],
  REMAP_TYPE_SORT_ORDER: ["mp4", "flv", "webm", "3gp", "audio/mp4"],
  get_sort_idx: function (arr, value) {
    // Unknown values go to the very end of the list.
    return arr.indexOf(value) & 0xff;
  },

  // http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
  // Map<String name, Array<int> itags> STREAM_REMAP_QUALITY_MAP;
  STREAM_REMAP_QUALITY_MAP: {"144p": [17, 160], "240p": [5, 36, 83, 133], "270p": [6], "360p": [18, 34, 43, 82, 100, 101, 134],
    "480p": [35, 44, 135], "520p": [85], "720p": [22, 45, 84, 102, 120, 136], "1080p": [37, 46, 137], "Original": [38, 138],
    /*audio*/ "48k": [139], "128k": [140, 171], "256k": [141, 172]
  },
  // Map<String name, Array<int> itags> STREAM_REMAP_TYPE_MAP;
  STREAM_REMAP_TYPE_MAP: {"mp4": [18, 22, 37, 38, 82, 83, 84, 85, 133, 134, 135, 136, 137, 138, 160],
    "flv": [5, 6, 34, 35, 120], "webm": [43, 44, 45, 46, 100, 101, 102], "3gp": [13, 17, 36],
    "audio/mp4": [139, 140, 141], "audio/webm": [171, 172]
  },
  // Map<String name, Array<int> itags> STREAM_QUALITY_MAP;
  STREAM_QUALITY_MAP: {"small": [5, 6, 17, 36, 83, 133, 139, 160], "medium": [18, 34, 43, 82, 100, 101, 134, 140, 171],
    "large": [35, 44, 85, 135, 141, 172], "hd720": [22, 45, 84, 102, 120, 136], "hd1080": [37, 46, 137], "highres": [38, 138]
  },
  // Map<String name, Array<int> itags> STREAM_TYPE_MAP;
  STREAM_TYPE_MAP: {"video/mp4": [18, 22, 37, 38, 82, 83, 84, 85, 133, 134, 135, 136, 137, 138, 160],
    "video/x-flv": [5, 6, 34, 35, 120], "video/webm": [43, 44, 45, 46, 100, 101, 102], "video/3gpp": [13, 17, 36],
    "audio/mp4": [139 /*48k*/, 140 /*125k*/, 141 /*250k*/], "audio/webm": [171 /*138k*/, 172 /*209k*/]
  },
  remap_stream_quality: function(stream) {
    return this._map_itag(this.STREAM_REMAP_QUALITY_MAP, stream.itag, stream.quality);
  },
  remap_stream_type: function(stream) {
    return this._map_itag(this.STREAM_REMAP_TYPE_MAP, stream.itag, stream.type);
  },
  _map_itag: function(map, itag, defaultValue) {
    itag = Number(itag);
    for (var p in map) {
      if (map[p].indexOf(itag) !== -1) {
        return p;
      }
    }
    return defaultValue;
  },
  get_stream_content_type: function(url) {
    if ( ! this.is_stream_url(url) || ! /[?&]itag=([^&]+)/.test(url)) { return null; }
    return this._map_itag(this.STREAM_TYPE_MAP, RegExp.$1, null);
  },

  // Removes junk parameters (like "fexp") from the stream URL.
  // We keep the following parameters:
  // 1) those listed in the "sparams" parameter
  // 2) "sparams"
  // 3) "signature"
  // 4) "key"
  // 5) "id" (internal? (as in private/non-public) video ID)
  // 6) "itag" (stream "ID" - container type and video quality)
  // 7) those passed in the |extra_keep_params| argument.
  // 8) anything that ends with "bypass" and has the value of "yes".
  tidy_stream_url: function (stream, extra_keep_params /*= null*/ ) {
    if (!extra_keep_params) { extra_keep_params = []; }
    var url = stream.url;
    if (!url) { return; }
    var qs_idx = url.indexOf("?");
    if (qs_idx === -1 || qs_idx + 1 === url.length) { return; }
    var qs = Youtube.parse(url.substr(qs_idx + 1));
    var sparams = qs["sparams"];
    if (!sparams) { return; }
    sparams = sparams.split(",");
    extra_keep_params.push("sparams", "signature", "key", "id", "itag");
    extra_keep_params.forEach(function(p){if (sparams.indexOf(p) === -1) sparams.push(p);});
    var new_qs = [];
    for (var p in qs) {
      if (sparams.indexOf(p) !== -1 || (/.+bypass$/.test(p) && qs[p] === "yes")) {
        new_qs.push(encodeURIComponent(p) + "=" + encodeURIComponent(qs[p] || ""));
      }
    }
    stream.url = url.substr(0, qs_idx + 1) + new_qs.join("&");
  }, // tidy_stream_url()

  // For YouTube, we can't rely on simple URL comparison because
  // the query strings of our parsed streams and the ones built by
  // the player can (and most likely, will) differ.
  stream_url_equals: function (url1, url2) {
    if (!this.is_stream_url(url1) || !this.is_stream_url(url2)) { return false; }

    // The |id| parameter is something like the internal video ID:
    // it's different for two different videos (those with different
    // "public" video IDs - /watch?v=VIDEO_ID), and it's the same for
    // all the streams of the same video.
    var id1 = /[?&]id=([^&]+)/.test(url1) ? RegExp.$1 : "flashgot-id-1";
    var id2 = /[?&]id=([^&]+)/.test(url2) ? RegExp.$1 : "flashgot-id-2";
    if (id1 !== id2) { return false; }

    // The only thing that allows us to identify a stream is the |itag|
    // parameter, which is a combination of the container type (MP4,
    // FLV, etc.) and the quality (1080p, 1080p 3D, 720p, etc.).
    var itag1 = /[?&]itag=([^&]+)/.test(url1) ? RegExp.$1 : "flashgot-itag-1";
    var itag2 = /[?&]itag=([^&]+)/.test(url2) ? RegExp.$1 : "flashgot-itag-2";
    if (itag1 !== itag2) { return false; }

    // Different seek positions are considered different streams (in case
    // the user wants to save the stream from a particular position).
    var begin1 = /[?&]begin=([^&]+)/.test(url1) ? RegExp.$1 : "0";
    var begin2 = /[?&]begin=([^&]+)/.test(url2) ? RegExp.$1 : "0";
    return begin1 === begin2;
  },

  is_stream_url: function (url) {
    return /^[a-z]+:\/\/([^\/]+)\/videoplayback\?/.test(url) && /\.(?:youtube|googlevideo)\.com$/.test(RegExp.$1);
  },

  decode_signature: function (params) {
    /* Not encoded. */
    return params.stream.sig || "";
  },
  decode_signature_swap: function (a, idx) {
    var tmp = a[0];
    a[0] = a[idx % a.length];
    a[idx] = tmp;
    return a;
  },

  create_signature_decoder: function () {
    var s = fg.getPref("media.YouTube.decode_signature_func", "");
    if (!s) {
      return new SignatureDecoder(Youtube.decode_signature);
    }
    // Fail fast: try to compile right now to check the code for
    // syntax errors, so that we don't do all that heavy stuff for
    // sandbox initialization only to fail later in evalInSandbox()
    // and have an incorrect error message saying "error _calling_
    // the function" while actually we failed to compile it.
    var func = new Function("params", s);
    if (fg.getPref("media.YouTube.decode_signature_func.sandbox", true)) {
      // Wrap the code into a function invocation because we promised
      // to call it as a function with one parameter.
      s = "(function(params){\n" + s + "\n})(params);";
      return new SandboxedSignatureDecoder(s);
    }
    return new SignatureDecoder(func);
  },


  refresh_signature_func: function (w, callback /*= null*/, force /*= false*/) {
    return SDASniffer.sniff(w, callback, force);
  }
}; // Youtube



///////////////////////////////////////////////////////////////////////////////
// Private part.

// interface ISignatureDecoder {
//   string decode(Params params);
//   void dispose();
// }
// class Params {
//   Map<string, string> stream;
//   Map<string, string> video_info;
//   Function swap; //Array swap(Array, int);
// }
//
// class SignatureDecoder implements ISignatureDecoder {
//   SignatureDecoder(Function func);
// }
function SignatureDecoder(func) {
  this.func = func;
}

SignatureDecoder.prototype = {
  decode: function(params) { return this.func(params); },
  dispose: function() { this.func = null; }
};



// class SandboxedSignatureDecoder implements ISignatureDecoder {
//   SandboxedSignatureDecoder(String code_str);
// }
function SandboxedSignatureDecoder(code_str) {
  this.code_str = code_str;

  this.sandbox = new Components.utils.Sandbox(this.SANDBOX_PRINCIPAL, this.SANDBOX_OPTIONS);

  // If you simply assign objects to the sandbox properties, they will
  // be visible, but their properties won't.
  // Primitive values and functions can be assigned as-is. (Although
  // converting a function to string will return the "standard" string
  // "function() { [native code] }" because none of its properties,
  // including the toString method, will be visible.)
  //   var obj = {stream: s, video_info: data, swap: ... };
  //   sandbox.obj = obj;
  //   sandbox.str = "hi";
  //   sandbox.num = 42;
  //   sandbox.fun = function(){};
  //   // Somewhere in decode_signature_func's global scope,
  //   // i.e. |this| points to |sandbox|:
  //   this.obj; // defined.
  //   this.obj.stream; // undefined.
  //   this.str; // defined.
  //   this.num; // defined.
  //   this.fun; // defined.
  this.sb_params = this.export_to_sandbox({
    stream: null,
    video_info: null,
    swap: Youtube.decode_signature_swap
  });
  this.sandbox.params = this.sb_params;

} // SandboxedSignatureDecoder()


SandboxedSignatureDecoder.prototype = {
  // https://developer.mozilla.org/en-US/docs/Security_check_basics:
  // The null principal (whose contract ID is @mozilla.org/nullprincipal;1)
  // fails almost all security checks. It has no privileges and can't be
  // accessed by anything but itself and chrome. They aren't same-origin
  // with anything but themselves.
  SANDBOX_PRINCIPAL: Components.classes["@mozilla.org/nullprincipal;1"]
    .createInstance(Components.interfaces.nsIPrincipal),

  SANDBOX_OPTIONS: {wantComponents: false, wantXHRConstructor: false},


  // Makes all the own properties of an object available for reading
  // in a sandbox.
  // Not recursive, i.e. doesn't go in depth, i.e. if a property
  // is an object as well, then its properties won't be processed.
  // Exposing actual objects and not their copies is safe because
  // the sandboxed code can't define getters/setters for their
  // properties because we expose them as read-only, nor can it
  // assign a watcher for a property because calling watch()
  // on the exposed objects throws "can't watch non-native
  // objects of class Proxy".
  //
  // [quote src="https://developer.mozilla.org/en/docs/XPConnect_wrappers#XPCChromeObjectWrapper"]
  // XPCChromeObjectWrapper. A chrome object wrapper, or COW, is automatically
  // created when a chrome object is exposed to content. If the chrome object
  // has an __exposedProps__ property, then content may only access the whitelisted
  // properties. This allows chrome to expose limited parts of objects to content
  // without having to use IDL.
  // The __exposedProps__ property is an object with a property corresponding
  // to each of the chrome object's properties which should be exposed. The value
  // of the properties within __exposedProps__ is either "w", "wr", or "r". These
  // will allow content to read and/or write the property (executing a function
  // is considered reading). Remember that functions defined in chrome context
  // execute with chrome privileges.
  // Gecko 15.0 note (Firefox 15.0 / Thunderbird 15.0 / SeaMonkey 2.12):
  //   Starting in Gecko 15.0 (Firefox 15.0 / Thunderbird 15.0 / SeaMonkey 2.12),
  //   exposing functions or objects without using __exposedProps__ is deprecated.
  //   Using __exposedProps__ will be mandatory starting with Gecko 17.0 (Firefox 17.0
  //   / Thunderbird 17.0 / SeaMonkey 2.14 / Firefox OS 1.0).
  // [/quote]
  export_to_sandbox: function (obj) {
    var props = {"toString": "r", "valueOf": "r"};
    for (var p in obj) {
      if (p !== "__exposedProps__" && obj.hasOwnProperty(p)) {
        props[p] = "r";
      }
    }
    obj.__exposedProps__ = props;
    return obj;
  }, // export_to_sandbox()


  decode: function (params) {
    this.sb_params.stream = this.export_to_sandbox(params.stream);

    // First call - export the video_info.
    if (!this.sb_params.video_info) {
      this.sb_params.video_info = this.export_to_sandbox(params.video_info);
    }

    var rc = Components.utils.evalInSandbox(this.code_str, this.sandbox);
    delete params.stream.__exposedProps__;
    // No fancy return values - we expect a primitive string value.
    // We don't silently return something that could pass for a signature.
    // Instead, we throw - to inform the user that their decode_signature_func
    // function is broken (anyone can make a typo) or malicious.
    //
    // It's OK to pass uncaught exceptions as-is because even if they have
    // getters for properties like "message", those will be executed in the
    // context of the sandbox (i.e. the global |this| will point to the sandbox),
    // which is useless for malicious code anyway.
    // Here's what am I talking about - somewhere in the sandboxed code:
    //   var x = new Error();
    //   x.__defineGetter__("message", function(){alert("pwned");});
    //   throw x;
    //   or:
    //   var x = { message: { valueOf: function(){alert("pwned");}, toString: function(){alert("pwned");} } };
    //   throw x;
    // We could catch the exceptions here, manually sanitize them and rethrow
    // if they're safe to use in our chrome code, but I just don't see the point
    // in doing so because if there's a bug in the security manager, then our
    // manual sanitization will just conceal it.
    if (typeof (rc) === "string") { return rc; }
    // Nulls are kinda OK.
    if (rc === null) { return ""; }
    // A forgotten return or outdated code that returns nonexistent stream
    // properties? Worth a warning in either case.
    if (rc === undefined) {
      fg.log("WARNING: YouTube.decode_signature_func returned undefined.");
      return "";
    }
    throw new Error("Invalid return value type: expected string, got " + typeof (rc));
  }, // decode()

  dispose: function () {
    if (!this.sandbox) { return; }
    this.sandbox.params = this.sb_params = null;
    Components.utils.nukeSandbox(this.sandbox);
    this.sandbox = null;
  }
}; // SandboxedSignatureDecoder.prototype



// Signature decoding algorithm (SDA) sniffer.
var SDASniffer = {
  // We don't want "over 9000" workers doing the same thing when one
  // is enough (can happen if we're restoring a session with several
  // YouTube tabs/windows).
  // static boolean working = false;
  // static Array<Function> callbacks = [];
  working: false,
  callbacks: [],

  sniff: function (w, callback /*= null*/, force /*= false*/) {
    if (typeof(callback) !== "function") { callback = null; }

    if (this.working) {
      if (callback) { this.callbacks.push(callback); }
      return true;
    }

    // Get the SWF player URL.
    w = w.wrappedJSObject;
    var swf_url;
    var o;
    // ytplayer.config.url
    if (w && (o = w.ytplayer) && (o = o.config)) {
      swf_url = o.url;
    }
    // yt.config_["PLAYER_CONFIG"].url
    else if (w && (o = w.yt) && (o = o.config_) && (o = o.PLAYER_CONFIG)) {
      swf_url = o.url;
    }
    if (!swf_url) { return false; }
    fg.log("SWF URL: " + swf_url);

    // Automatic update frequency is limited so that we waste less traffic
    // and CPU cycles in case YoutubeSwf code is outdated.
    if ( ! force) {
      var now = Math.floor(Date.now() / 1000);
      var min_int = fg.getPref("media.YouTube.decode_signature_func.auto.min_interval", 60);
      var last_update = fg.getPref("media.YouTube.decode_signature_func.auto.last_update_time", 0);
      if (min_int !== 0 && now - last_update < min_int) {
        if ( ! fg.getPref("media.YouTube.decode_signature_func.auto.last_update_ok")) {
          return false;
        }
        // We promised to be async, so we can't call back _before_ we return,
        // hence setTimeout.
        w.setTimeout(function(){
          try {
            callback();
          } catch (x) {
            fg.log("Callback error: " + (x.message || x) + "\n" + x.stack);
          }
        }, 1);
        return true;
      }
      fg.setPref("media.YouTube.decode_signature_func.auto.last_update_time", now);
    }

    var st, ft;
    var stream_ctx = {
      file: swf_url, //.split("/").pop().replace(/\?.*$/, "").replace(/#.*$/, ""),
      bytes: "",
      contentLength: -1,
      bstream: null
    };
    var stream_listener = {
      onDataAvailable: function (req, ctx, stream, offset, count) {
        stream_ctx.bstream.setInputStream(stream);
        stream_ctx.bytes += stream_ctx.bstream.readBytes(count);
      },
      onStartRequest: function (req /*, ctx*/) {
        var channel = req.QueryInterface(Components.interfaces.nsIChannel);
        if (!((channel instanceof Components.interfaces.nsIHttpChannel)
          && Components.isSuccessCode(channel.status)
          && channel.responseStatus === 200))
        {
          throw new Error("cancel"); //req.cancel(NS_BINDING_ABORTED);
        }
        stream_ctx.contentLength = channel.contentLength || -1;
        fg.log("SWF content length: " + stream_ctx.contentLength);
        stream_ctx.bstream = Components.classes["@mozilla.org/binaryinputstream;1"]
          .createInstance(Components.interfaces.nsIBinaryInputStream);
        st = Date.now();
      },
      onStopRequest: function (req, ctx, status) {
        ft = Date.now();
        stream_ctx.bstream = null;
        // SDASniffer::sniff0 is async, so we can't simply do if (SDASniffer.working) {clean up}.
        var cleanup = true;
        if (Components.isSuccessCode(status)) {
          fg.log("SWF downloaded in " + (ft - st) + " ms, size: " + stream_ctx.bytes.length);
          if (stream_ctx.contentLength === -1 || stream_ctx.bytes.length === stream_ctx.contentLength) {
            SDASniffer.sniff0(stream_ctx, callback);
            cleanup = false;
          }
          else {
            fg.log("SWF content length mismatch: expected " + stream_ctx.contentLength + ", got " + stream_ctx.bytes.length);
          }
        }
        else {
          fg.log("Failed to download the SWF: status=" + status);
        }
        stream_ctx = null;
        if (cleanup) {
          SDASniffer.working = false;
          SDASniffer.callbacks = [];
        }
      }
    }; // stream_listener
    Components.classes["@mozilla.org/network/io-service;1"]
      .getService(Components.interfaces.nsIIOService)
      .newChannel(swf_url, null, null)
      .asyncOpen(stream_listener, null);

    this.working = true;
    if (callback) { this.callbacks.push(callback); }
    fg.setPref("media.YouTube.decode_signature_func.auto.last_update_ok", false);
    return true;
  },


  sniff0: function (ctx, callback) {
    // Using a worker instead of a direct call resolves the problem
    // with GUI freezing due to severe performance degradation: 100 ms
    // vs 2400 ms for zip_inflate(), 100 ms vs 800 ms for swf_parse().
    // See bug 911570 (https://bugzilla.mozilla.org/show_bug.cgi?id=911570),
    // or 776798, or 907201, or whatever is causing it.
    var worker = new SDAWorker( {bytes: ctx.bytes, file: ctx.file} );
    ctx.bytes = null;

    worker.onfinish = function(rc) {
      SDASniffer.working = false;
      var callbacks = SDASniffer.callbacks;
      SDASniffer.callbacks = [];

      if (typeof(rc) === "string") {
        fg.log("Error refreshing signature function: " + rc);
        return;
      }

      if ( ! rc) { return; }
      fg.setPref("media.YouTube.decode_signature_func.auto.last_update_ok", true);

      if (rc.timestamp !== fg.getPref("media.YouTube.decode_signature_func.timestamp")) {
        fg.log("New timestamp: " + rc.timestamp);
        fg.setPref("media.YouTube.decode_signature_func.timestamp", rc.timestamp);
      }

      if (rc.func_text !== fg.getPref("media.YouTube.decode_signature_func")) {
        fg.log("New signature function:\n" + rc.func_text);
        fg.setPref("media.YouTube.decode_signature_func", rc.func_text);
        callbacks.forEach(function(f){
          try {
            f();
          } catch (x) {
            fg.log("Callback error: " + (x.message || x) + "\n" + x.stack);
          }
        });
      }
    };

    try {
      worker.start();
    } catch (x) {
      worker.onfinish("Error starting the worker: " + (x.message || x) + "\n" + x.stack);
    }
  }
}; // SDASniffer



// class SDAWorker;
function SDAWorker(ctx) {
  this.ctx = ctx;
  this.worker = null;
  this.fired_onfinish = false;
}

SDAWorker.prototype = {
  // public
  start: function() {
    var worker = this.worker = new Worker("YoutubeSwf.js");
    worker["SDAWorker::this"] = this;
    worker.onmessage = this.worker_onmessage;
    worker.onerror = this.worker_onerror;
    worker.postMessage(this.ctx);
    this.ctx = null;
  },

  // Completion event handler, implemented by the caller.
  // void onfinish(Object data);
  // @param data - the result of the decoding, one of:
  //   1) a primitive string value (typeof data === "string") - there was
  //      an uncaught exception in the worker, and |data| is the error message.
  //   2) Object - struct { string func_text; int timestamp; } - the result
  //      of the decoding. Can be null/undefined (data == null covers both)
  //      if the signature function could not be decoded.
  onfinish: function(){},


  // private
  fire_onfinish: function(data) {
    this.fired_onfinish = true;
    try {
      this.onfinish(data);
    } catch (x) {
      fg.log("Error in onfinish: " + (x.message || x) + "\n" + x.stack);
    }
  },

  worker_onmessage: function(evt) {
    var This = this["SDAWorker::this"];
    // struct msg { string type; Object data; };
    var msg = evt.data;
    if (msg == null) {
      fg.log("SDAWorker: Invalid message: null or undefined: " + msg);
      This.finish();
      return;
    }
    if (typeof(msg) !== "object") {
      fg.log("SDAWorker: Invalid message: expected [object], got [" + typeof(msg) + "]: " + msg);
      This.finish();
      return;
    }
    switch (msg.type) {
      case "done":
        This.finish();
        return;
      case "log":
        fg.log(msg.data);
        return;
      case "result":
        This.fire_onfinish(msg.data);
        return;
    }
    fg.log("SDAWorker: Invalid message type: '" + msg.type + "'");
    This.finish();
  },

  worker_onerror: function(evt) {
    var This = this["SDAWorker::this"];
    This.fire_onfinish("Uncaught exception in worker: " + evt.message);
    This.finish();
  },

  finish: function() {
    if ( ! this.fired_onfinish) {
      this.fire_onfinish(null);
    }
    try {
      this.worker.terminate();
      this.worker["SDAWorker::this"]
        = this.worker.onmessage
        = this.worker.onerror
        = null;
      this.worker = null;
    } catch (x) {
      fg.log("Error terminating the worker: " + (x.message || x) + "\n" + x.stack);
    }
  }
}; // SDAWorker.prototype


return Youtube;
}(); // Youtube
